import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
import warnings
from great_tables import GT
warnings.filterwarnings('ignore')
# Настройки для визуализаций
plt.style.use('seaborn-v0_8-whitegrid')
# Монохромная палитра с красными акцентами
colors = ['#808080', '#606060', '#404040', '#FF6B6B', '#CC5555']
sns.set_palette(colors)
plt.rcParams['font.size'] = 11
plt.rcParams['figure.titlesize'] = 14
plt.rcParams['axes.titlesize'] = 12
plt.rcParams['axes.labelsize'] = 11Анализ данных о сердечно-сосудистых заболеваниях (поиск инсайтов, составление рекомендаций стейкхолдерам)
В данном исследовании проводится комплексный анализ данных о сердечно-сосудистых заболеваниях с целью выявления ключевых факторов риска и построения предиктивных моделей. Анализ включает исследовательский анализ данных, разработку и сравнение моделей машинного обучения для прогнозирования наличия сердечно-сосудистых заболеваний.
Введение
Сердечно-сосудистые заболевания являются основной причиной смертности во многих странах мира. Раннее выявление факторов риска и своевременная профилактика играют ключевую роль в снижении заболеваемости и смертности.
Цель исследования
Основной целью данного исследования является анализ факторов риска сердечно-сосудистых заболеваний на основе данных медицинских обследований и построение предиктивных моделей для оценки вероятности наличия заболевания.
Задачи исследования
- Провести исследовательский анализ данных для выявления ключевых закономерностей
- Выполнить очистку и предобработку данных
- Построить и оценить предиктивные модели
- Сформулировать практические рекомендации для заинтересованных лиц
Основные стейкхолдеры
1. Медицинская лаборатория
Приоритеты: - Повышение точности диагностики сердечно-сосудистых заболеваний - Оптимизация скрининговых программ - Снижение затрат на обработку данных - Улучшение качества предоставляемых услуг
Задачи: - Внедрение предиктивных моделей в рутинную практику - Обучение персонала работе с ML-инструментами - Интеграция моделей в существующие лабораторные системы - Мониторинг эффективности внедренных решений
2. Врачи-кардиологи и терапевты
Приоритеты: - Получение точных инструментов для оценки риска пациентов - Сокращение времени на принятие клинических решений - Повышение качества лечения и профилактики - Снижение пропускной способности высокорисковых пациентов
Задачи: - Использование предиктивных моделей в клинической практике - Интерпретация результатов ML-моделей для пациентов - Адапация рекомендаций под индивидуальные особенности пациентов - Обеспечение этического использования алгоритмов
3. Пациенты
Приоритеты: - Своевременное выявление рисков сердечно-сосудистых заболеваний - Получение персонализированных рекомендаций - Повышение качества жизни и здоровья - Снижение тревожности относительно состояния здоровья
Задачи: - Прохождение регулярных обследований - Следование рекомендациям по изменению образа жизни - Активное участие в программах мониторинга здоровья - Соблюдение предписанного лечения
4. Система здравоохранения
Приоритеты: - Снижение общей заболеваемости и смертности от ССЗ - Оптимизация распределения медицинских ресурсов - Повышение эффективности профилактических программ - Снижение экономических затрат на лечение ССЗ
Задачи: - Разработка и внедрение национальных скрининговых программ - Создание реестров пациентов с высоким риском - Обеспечение доступности качественной медицинской помощи - Мониторинг популяционных показателей здоровья
5. Страховые компании
Приоритеты: - Снижение выплат по дорогостоящим случаям лечения ССЗ - Оптимизация тарифов страховых продуктов - Повышение удержания клиентов через профилактические программы - Точный расчет актуарных рисков
Задачи: - Разработка программ превентивной медицины - Интеграция моделей оценки рисков в андеррайтинг - Создание стимулов для здорового образа жизни клиентов - Мониторинг медицинских расходов клиентов
6. Исследователи и академическое сообщество
Приоритеты: - Получение новых научных знаний о факторах риска ССЗ - Валидация методологий машинного обучения в медицине - Публикация результатов в рецензируемых журналах - Развитие междисциплинарного сотрудничества
Задачи: - Проведение дополнительных исследований на расширенных данных - Валидация моделей на независимых выборках - Разработка новых методологий анализа - Подготовка научных публикаций и презентаций
7. Разработчики медицинских технологий
Приоритеты: - Создание коммерчески жизнеспособных продуктов - Обеспечение соответствия регуляторным требованиям - Масштабирование решений для широкого использования - Поддержание конкурентоспособности на рынке
Задачи: - Разработка пользовательских интерфейсов для клиницистов - Интеграция с существующими медицинскими системами (HIS/EMR) - Обеспечение безопасности и конфиденциальности данных - Проведение клинических испытаний и сертификация
Обзор данных
В исследовании используется датасет Cardiovascular Disease Dataset, содержащий информацию о 70 000 пациентах. Данные предоставлены медицинской лабораторией и включают 11 признаков и целевую переменную наличия сердечно-сосудистого заболевания.
Описание признаков
- age - возраст в днях
- gender - пол (1 - женщина, 2 - мужчина)
- height - рост в см
- weight - вес в кг
- ap_hi - систолическое артериальное давление
- ap_lo - диастолическое артериальное давление
- cholesterol - уровень холестерина (1: нормальный, 2: выше нормы, 3: высокий)
- gluc - уровень глюкозы (1: нормальный, 2: выше нормы, 3: высокий)
- smoke - курение (0: нет, 1: да)
- alco - употребление алкоголя (0: нет, 1: да)
- active - физическая активность (0: нет, 1: да)
- cardio - наличие сердечно-сосудистого заболевания (0: нет, 1: да)
Методология
Подходы к анализу
Исследование будет проводиться в несколько этапов:
- Исследовательский анализ данных (EDA): анализ распределений, выявление выбросов, изучение взаимосвязей
- Предобработка данных: очистка, нормализация, создание новых признаков
- Моделирование: построение и сравнение моделей машинного обучения
- Интерпретация результатов: анализ важности признаков и формулирование выводов
Инструменты анализа
- Python 3.12+ с научными библиотеками pandas, numpy, matplotlib, seaborn
- scikit-learn для построения моделей машинного обучения
- Quarto для генерации отчета
Результаты EDA
Настройка окружения
Для начала импортируем необходимые библиотеки и настроим параметры визуализации.
Загрузка данных
Загрузим набор данных и выведем основную информацию о его размере.
# Загрузка данных
data_path = 'data/cardio_train.csv'
df = pd.read_csv(data_path, sep=';')
# Базовая информация
print(f"Размер датасета: {df.shape}")Размер датасета: (70000, 13)
Предварительный просмотр
Ознакомимся со структурой данных, посмотрев на первые несколько строк.
GT(df.head())| id | age | gender | height | weight | ap_hi | ap_lo | cholesterol | gluc | smoke | alco | active | cardio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 18393 | 2 | 168 | 62.0 | 110 | 80 | 1 | 1 | 0 | 0 | 1 | 0 |
| 1 | 20228 | 1 | 156 | 85.0 | 140 | 90 | 3 | 1 | 0 | 0 | 1 | 1 |
| 2 | 18857 | 1 | 165 | 64.0 | 130 | 70 | 3 | 1 | 0 | 0 | 0 | 1 |
| 3 | 17623 | 2 | 169 | 82.0 | 150 | 100 | 1 | 1 | 0 | 0 | 1 | 1 |
| 4 | 17474 | 1 | 156 | 56.0 | 100 | 60 | 1 | 1 | 0 | 0 | 0 | 0 |
Типы данных
Проверим типы данных каждого признака, чтобы убедиться в их корректности.
types_df = df.dtypes.reset_index()
types_df.columns = ["Признак", "Тип данных"]
GT(types_df)| Признак | Тип данных |
|---|---|
| id | int64 |
| age | int64 |
| gender | int64 |
| height | int64 |
| weight | float64 |
| ap_hi | int64 |
| ap_lo | int64 |
| cholesterol | int64 |
| gluc | int64 |
| smoke | int64 |
| alco | int64 |
| active | int64 |
| cardio | int64 |
Описательная статистика
Рассмотрим основные статистические характеристики числовых признаков.
stats_df = df.describe().reset_index()
GT(stats_df)| index | id | age | gender | height | weight | ap_hi | ap_lo | cholesterol | gluc | smoke | alco | active | cardio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 |
| mean | 49972.4199 | 19468.865814285713 | 1.3495714285714286 | 164.35922857142856 | 74.20569 | 128.8172857142857 | 96.63041428571428 | 1.3668714285714285 | 1.226457142857143 | 0.08812857142857143 | 0.053771428571428574 | 0.8037285714285715 | 0.4997 |
| std | 28851.30232317292 | 2467.2516672414013 | 0.47683801558286387 | 8.210126364538038 | 14.395756678511379 | 154.01141945609137 | 188.47253029639026 | 0.680250348699381 | 0.572270276613845 | 0.28348381676993517 | 0.2255677036041049 | 0.3971790635049283 | 0.5000034814661862 |
| min | 0.0 | 10798.0 | 1.0 | 55.0 | 10.0 | -150.0 | -70.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 25% | 25006.75 | 17664.0 | 1.0 | 159.0 | 65.0 | 120.0 | 80.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 50% | 50001.5 | 19703.0 | 1.0 | 165.0 | 72.0 | 120.0 | 80.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 75% | 74889.25 | 21327.0 | 2.0 | 170.0 | 82.0 | 140.0 | 90.0 | 2.0 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 |
| max | 99999.0 | 23713.0 | 2.0 | 250.0 | 200.0 | 16020.0 | 11000.0 | 3.0 | 3.0 | 1.0 | 1.0 | 1.0 | 1.0 |
Проверка на пропуски
Важным этапом является проверка данных на наличие пропущенных значений.
# Проверка пропусков
missing_values = df.isnull().sum().reset_index()
missing_values.columns = ["Признак", "Количество пропусков"]
if missing_values["Количество пропусков"].sum() == 0:
print("Пропусков не обнаружено")
else:
GT(missing_values[missing_values["Количество пропусков"] > 0])Пропусков не обнаружено
Проверка дубликатов
Проверим наличие полных дубликатов записей, которые могут исказить результаты анализа, и удалим их при наличии.
# Проверка дубликатов
duplicates = df.duplicated().sum()
print(f"Количество полных дубликатов: {duplicates}")
# Удаление дубликатов если есть
if duplicates > 0:
df = df.drop_duplicates()
print(f"После удаления дубликатов размер: {df.shape}")Количество полных дубликатов: 0
Анализ категориальных признаков (структура)
Посмотрим на уникальные значения в категориальных переменных для понимания их структуры.
categorical_cols = ['gender', 'cholesterol', 'gluc', 'smoke', 'alco', 'active', 'cardio']
unique_data = []
for col in categorical_cols:
unique_vals = sorted(df[col].unique())
unique_data.append({"Признак": col, "Уникальные значения": str(unique_vals)})
GT(pd.DataFrame(unique_data))| Признак | Уникальные значения |
|---|---|
| gender | [np.int64(1), np.int64(2)] |
| cholesterol | [np.int64(1), np.int64(2), np.int64(3)] |
| gluc | [np.int64(1), np.int64(2), np.int64(3)] |
| smoke | [np.int64(0), np.int64(1)] |
| alco | [np.int64(0), np.int64(1)] |
| active | [np.int64(0), np.int64(1)] |
| cardio | [np.int64(0), np.int64(1)] |
Распределение целевой переменной
Проанализируем сбалансированность классов целевой переменной cardio. Это важно для выбора метрик оценки моделей.
plt.figure(figsize=(8, 6))
ax = sns.countplot(data=df, x='cardio', palette=['#808080', '#FF6B6B'])
plt.title('Распределение наличия сердечно-сосудистых заболеваний', fontsize=14, pad=20)
plt.xlabel('Наличие заболевания (0 - нет, 1 - да)', fontsize=12)
plt.ylabel('Количество пациентов', fontsize=12)
# Добавление процентов
total = len(df)
for p in ax.patches:
percentage = f'{100 * p.get_height() / total:.1f}%'
ax.annotate(percentage, (p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom', fontsize=11)
plt.tight_layout()
plt.show()Детальная статистика распределения целевой переменной:
target_stats = df['cardio'].value_counts().reset_index()
target_stats.columns = ['Cardio', 'Count']
target_stats['Percentage'] = (target_stats['Count'] / total * 100).round(1).astype(str) + '%'
GT(target_stats)| Cardio | Count | Percentage |
|---|---|---|
| 0 | 35021 | 50.0% |
| 1 | 34979 | 50.0% |
Распределения числовых признаков
Для удобства анализа преобразуем возраст из дней в годы.
df['age_years'] = df['age'] / 365.25Возраст
Рассмотрим распределение возраста пациентов.
Рост
Анализ распределения роста пациентов.
Вес
Анализ распределения веса пациентов.
Систолическое давление
Распределение верхнего (систолического) артериального давления.
Диастолическое давление
Распределение нижнего (диастолического) артериального давления.
Распределения категориальных признаков
Проанализируем категориальные факторы риска.
Пол
Соотношение мужчин и женщин в выборке.
gender_counts = df['gender'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Женщины', 'Мужчины'], y=gender_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Распределение по полу', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Холестерин
Уровни холестерина среди пациентов.
cholesterol_counts = df['cholesterol'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
plt.bar(['Норма', 'Выше нормы', 'Высокий'], cholesterol_counts.values,
color=['#808080', '#606060', '#FF6B6B'])
plt.title('Уровень холестерина', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Глюкоза
Уровни глюкозы среди пациентов.
gluc_counts = df['gluc'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
plt.bar(['Норма', 'Выше нормы', 'Высокий'], gluc_counts.values,
color=['#808080', '#606060', '#FF6B6B'])
plt.title('Уровень глюкозы', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Курение
Доля курящих пациентов.
Алкоголь
Доля пациентов, употребляющих алкоголь.
alco_counts = df['alco'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Не употребляют', 'Употребляют'], y=alco_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Употребление алкоголя', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Физическая активность
Уровень физической активности пациентов.
active_counts = df['active'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Неактивны', 'Активны'], y=active_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Физическая активность', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Корреляционный анализ
Изучим линейные взаимосвязи между признаками, построив матрицу корреляций.
# Подготовка данных для корреляции
df_corr = df.drop(['id'], axis=1)
# Расчет корреляционной матрицы
correlation_matrix = df_corr.corr()
# Создание маски для верхней треугольной части
mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, mask=mask, annot=True, cmap='RdYlBu_r', center=0,
square=True, fmt='.2f', cbar_kws={"shrink": .8})
plt.title('Корреляционная матрица признаков', fontsize=16, pad=20)
plt.tight_layout()
plt.show()Выделим наиболее сильные корреляции для детального рассмотрения.
strong_correlations = []
for i in range(len(correlation_matrix.columns)):
for j in range(i):
if abs(correlation_matrix.iloc[i, j]) > 0.3:
strong_correlations.append({
'Пара признаков': f"{correlation_matrix.columns[i]} - {correlation_matrix.columns[j]}",
'Коэффициент корреляции': correlation_matrix.iloc[i, j]
})
GT(pd.DataFrame(strong_correlations))| Пара признаков | Коэффициент корреляции |
|---|---|
| height - gender | 0.4990334284422381 |
| gluc - cholesterol | 0.4515775236757577 |
| smoke - gender | 0.33813513635809417 |
| alco - smoke | 0.34009376786968487 |
| age_years - age | 0.9999999999999968 |
Анализ выбросов
Используем диаграммы размаха (boxplot) для выявления аномальных значений в числовых признаках.
Выбросы: Возраст
Выбросы: Рост
Выбросы: Вес
Выбросы: Систолическое давление
Выбросы: Диастолическое давление
plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='ap_lo', color='#808080')
plt.title('Box Plot: Диастолическое давление', fontsize=14)
plt.xlabel('мм рт.ст.')
plt.show()Количественная оценка выбросов по методу межквартильного размаха (IQR).
numeric_features = ['age_years', 'height', 'weight', 'ap_hi', 'ap_lo']
outliers_data = []
for feature in numeric_features:
Q1 = df[feature].quantile(0.25)
Q3 = df[feature].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_count = len(df[(df[feature] < lower_bound) | (df[feature] > upper_bound)])
outliers_data.append({
'Признак': feature,
'Количество выбросов': outliers_count,
'Процент': f"{outliers_count/len(df)*100:.1f}%"
})
GT(pd.DataFrame(outliers_data))| Признак | Количество выбросов | Процент |
|---|---|---|
| age_years | 4 | 0.0% |
| height | 519 | 0.7% |
| weight | 1819 | 2.6% |
| ap_hi | 1435 | 2.1% |
| ap_lo | 4632 | 6.6% |
Очистка данных
На основе EDA проведем очистку данных от аномальных и нереалистичных значений.
Инициализация
Создадим копию датафрейма для очистки.
df_clean = df.copy()
print(f"Исходный размер датасета: {df_clean.shape}")Исходный размер датасета: (70000, 14)
Очистка артериального давления
Фильтрация нереалистичных значений давления. Используем следующие критерии: - Систолическое: 70-250 мм рт.ст. - Диастолическое: 40-150 мм рт.ст. - Систолическое должно быть выше диастолического.
before_pressure = len(df_clean)
df_clean = df_clean[
(df_clean['ap_hi'] >= 70) & (df_clean['ap_hi'] <= 250) &
(df_clean['ap_lo'] >= 40) & (df_clean['ap_lo'] <= 150) &
(df_clean['ap_hi'] > df_clean['ap_lo'])
]
after_pressure = len(df_clean)
print(f"Удалено записей с нереалистичным давлением: {before_pressure - after_pressure}")Удалено записей с нереалистичным давлением: 1334
Очистка антропометрических данных
Фильтрация по росту и весу: - Рост: 100-220 см - Вес: 30-250 кг
before_anthro = len(df_clean)
df_clean = df_clean[
(df_clean['height'] >= 100) & (df_clean['height'] <= 220) &
(df_clean['weight'] >= 30) & (df_clean['weight'] <= 250)
]
after_anthro = len(df_clean)
print(f"Удалено записей с нереалистичным ростом/весом: {before_anthro - after_anthro}")Удалено записей с нереалистичным ростом/весом: 33
Очистка возраста
Оставляем пациентов от 18 до 100 лет.
before_age = len(df_clean)
df_clean = df_clean[
(df_clean['age_years'] >= 18) & (df_clean['age_years'] <= 100)
]
after_age = len(df_clean)
print(f"Удалено записей с нереалистичным возрастом: {before_age - after_age}")Удалено записей с нереалистичным возрастом: 0
Расчет BMI
Рассчитаем индекс массы тела (BMI) для дальнейшего анализа.
df_clean['bmi'] = df_clean['weight'] / (df_clean['height'] / 100) ** 2
print(f"Итоговый размер после очистки: {df_clean.shape}")Итоговый размер после очистки: (68633, 15)
Статистика очищенного датасета:
clean_stats = df_clean[['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']].describe().reset_index()
GT(clean_stats)| index | age_years | height | weight | ap_hi | ap_lo | bmi |
|---|---|---|---|---|---|---|
| count | 68633.0 | 68633.0 | 68633.0 | 68633.0 | 68633.0 | 68633.0 |
| mean | 53.291214957737346 | 164.3946206635292 | 74.11911034050677 | 126.67120772806085 | 81.30172074657963 | 27.473124357736904 |
| std | 6.757253833720525 | 7.977184812426184 | 14.307359581664704 | 16.681362962533587 | 9.422616258222744 | 5.351510180908495 |
| min | 29.56331279945243 | 100.0 | 30.0 | 70.0 | 40.0 | 10.726643598615919 |
| 25% | 48.34496919917864 | 159.0 | 65.0 | 120.0 | 80.0 | 23.875114784205696 |
| 50% | 53.93839835728953 | 165.0 | 72.0 | 120.0 | 80.0 | 26.346494034400994 |
| 75% | 58.38193018480493 | 170.0 | 82.0 | 140.0 | 90.0 | 30.119375573921033 |
| max | 64.92265571526352 | 207.0 | 200.0 | 240.0 | 150.0 | 152.55177514792896 |
Анализ BMI и категоризация
Распределение BMI
Посмотрим на распределение индекса массы тела в очищенной выборке.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='bmi', bins=30, color='#808080', alpha=0.7)
plt.axvline(x=18.5, color='blue', linestyle='--', alpha=0.7, label='Недостаточный вес')
plt.axvline(x=25, color='green', linestyle='--', alpha=0.7, label='Норма')
plt.axvline(x=30, color='orange', linestyle='--', alpha=0.7, label='Избыточный вес')
plt.axvline(x=35, color='red', linestyle='--', alpha=0.7, label='Ожирение')
plt.title('Распределение BMI', fontsize=14)
plt.xlabel('BMI')
plt.ylabel('Частота')
plt.legend()
plt.show()Категории BMI
Разделим пациентов на группы согласно классификации ВОЗ.
def categorize_bmi(bmi):
if bmi < 18.5:
return 'Недостаточный вес'
elif bmi < 25:
return 'Норма'
elif bmi < 30:
return 'Избыточный вес'
elif bmi < 35:
return 'Ожирение I степени'
else:
return 'Ожирение II+ степени'
df_clean['bmi_category'] = df_clean['bmi'].apply(categorize_bmi)
bmi_counts = df_clean['bmi_category'].value_counts()Визуализация распределения по категориям:
colors_bmi = ['#404040', '#606060', '#808080', '#FF6B6B', '#CC5555']
plt.figure(figsize=(10, 6))
sns.barplot(x=bmi_counts.index, y=bmi_counts.values, palette=colors_bmi)
plt.title('Категории BMI', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Детальная статистика по категориям BMI:
bmi_table = bmi_counts.reset_index()
bmi_table.columns = ['Категория', 'Количество']
bmi_table['Доля'] = (bmi_table['Количество'] / len(df_clean) * 100).round(1).astype(str) + '%'
GT(bmi_table)| Категория | Количество | Доля |
|---|---|---|
| Норма | 25424 | 37.0% |
| Избыточный вес | 24620 | 35.9% |
| Ожирение I степени | 11938 | 17.4% |
| Ожирение II+ степени | 6015 | 8.8% |
| Недостаточный вес | 636 | 0.9% |
Построение моделей
Подготовка данных для моделирования
Импорт необходимых библиотек для машинного обучения.
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, roc_curve,
confusion_matrix, classification_report)Разделение данных на матрицу признаков (X) и целевой вектор (y).
# Удаляем нерелевантные признаки и подготовляем X, y
X = df_clean.drop(['id', 'age', 'cardio', 'bmi_category'], axis=1)
y = df_clean['cardio']
print(f"Признаки для моделирования: {list(X.columns)}")
print(f"Размер признакового пространства: {X.shape}")Признаки для моделирования: ['gender', 'height', 'weight', 'ap_hi', 'ap_lo', 'cholesterol', 'gluc', 'smoke', 'alco', 'active', 'age_years', 'bmi']
Размер признакового пространства: (68633, 12)
Разделение на обучающую и тестовую выборки.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Размер обучающей выборки: {X_train.shape}")
print(f"Размер тестовой выборки: {X_test.shape}")Размер обучающей выборки: (54906, 12)
Размер тестовой выборки: (13727, 12)
Стандартизация числовых признаков для улучшения работы линейных моделей.
numeric_features = ['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']
scaler = StandardScaler()
X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()
X_train_scaled[numeric_features] = scaler.fit_transform(X_train[numeric_features])
X_test_scaled[numeric_features] = scaler.transform(X_test[numeric_features])
print("Числовые признаки стандартизированы")Числовые признаки стандартизированы
Обучение моделей
Настройка кросс-валидации.
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)Logistic Regression
Обучение логистической регрессии как базовой модели.
print("Обучение Logistic Regression...")
lr_model = LogisticRegression(random_state=42, max_iter=1000)
# Cross-validation
lr_cv_scores = cross_val_score(lr_model, X_train_scaled, y_train, cv=cv, scoring='roc_auc')
print(f"Logistic Regression CV AUC: {lr_cv_scores.mean():.4f} ± {lr_cv_scores.std():.4f}")
# Обучение на полных данных
lr_model.fit(X_train_scaled, y_train)Обучение Logistic Regression...
Logistic Regression CV AUC: 0.7900 ± 0.0031
LogisticRegression(max_iter=1000, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
Random Forest
Обучение случайного леса для выявления нелинейных зависимостей.
print("Обучение Random Forest...")
rf_model = RandomForestClassifier(n_estimators=100, random_state=42, max_depth=10)
# Cross-validation
rf_cv_scores = cross_val_score(rf_model, X_train, y_train, cv=cv, scoring='roc_auc')
print(f"Random Forest CV AUC: {rf_cv_scores.mean():.4f} ± {rf_cv_scores.std():.4f}")
# Обучение на полных данных
rf_model.fit(X_train, y_train)Обучение Random Forest...
Random Forest CV AUC: 0.7989 ± 0.0038
RandomForestClassifier(max_depth=10, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
Оценка качества моделей
Определим функцию для расчета метрик.
def evaluate_model(model, X_test_data, y_test_data, model_name):
"""Функция для оценки модели"""
# Предсказания
y_pred = model.predict(X_test_data)
y_pred_proba = model.predict_proba(X_test_data)[:, 1]
# Метрики
cm = confusion_matrix(y_test_data, y_pred)
tn, fp, fn, tp = cm.ravel()
specificity = tn / (tn + fp)
metrics = {
'Accuracy': accuracy_score(y_test_data, y_pred),
'Precision': precision_score(y_test_data, y_pred),
'Recall': recall_score(y_test_data, y_pred),
'F1-Score': f1_score(y_test_data, y_pred),
'ROC-AUC': roc_auc_score(y_test_data, y_pred_proba),
'Specificity': specificity
}
return metrics, y_pred, y_pred_probaПолучение метрик для обеих моделей.
lr_metrics, lr_pred, lr_pred_proba = evaluate_model(
lr_model, X_test_scaled, y_test, "Logistic Regression"
)
rf_metrics, rf_pred, rf_pred_proba = evaluate_model(
rf_model, X_test, y_test, "Random Forest"
)Сравнение метрик (График)
Визуальное сравнение основных метрик моделей.
metrics_comparison = pd.DataFrame({
'Logistic Regression': lr_metrics,
'Random Forest': rf_metrics
}).T
plt.figure(figsize=(10, 6))
metrics_comparison.plot(kind='bar', color=['#808080', '#FF6B6B', '#606060', '#404040', '#CC5555', '#909090'])
plt.title('Сравнение метрик качества моделей', fontsize=14, pad=20)
plt.xlabel('Модель', fontsize=12)
plt.ylabel('Значение метрики', fontsize=12)
plt.legend(title='Метрики', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()<Figure size 960x576 with 0 Axes>
Сравнение метрик качества моделей
Сравнение метрик (Таблица)
Детальная таблица со значениями метрик.
GT(metrics_comparison.reset_index().rename(columns={'index': 'Модель'}).round(4))| Модель | Accuracy | Precision | Recall | F1-Score | ROC-AUC | Specificity |
|---|---|---|---|---|---|---|
| Logistic Regression | 0.7275 | 0.7551 | 0.6647 | 0.707 | 0.7961 | 0.7889 |
| Random Forest | 0.7355 | 0.7656 | 0.6706 | 0.715 | 0.8056 | 0.799 |
ROC-кривые
Сравнение способности моделей разделять классы с помощью ROC-анализа.
plt.figure(figsize=(10, 8))
# Logistic Regression
fpr_lr, tpr_lr, _ = roc_curve(y_test, lr_pred_proba)
auc_lr = roc_auc_score(y_test, lr_pred_proba)
plt.plot(fpr_lr, tpr_lr, color='#808080', lw=2,
label=f'Logistic Regression (AUC = {auc_lr:.3f})')
# Random Forest
fpr_rf, tpr_rf, _ = roc_curve(y_test, rf_pred_proba)
auc_rf = roc_auc_score(y_test, rf_pred_proba)
plt.plot(fpr_rf, tpr_rf, color='#FF6B6B', lw=2,
label=f'Random Forest (AUC = {auc_rf:.3f})')
# Диагональ
plt.plot([0, 1], [0, 1], color='black', lw=1, linestyle='--', alpha=0.7)
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate', fontsize=12)
plt.ylabel('True Positive Rate', fontsize=12)
plt.title('ROC-кривые для сравнения моделей', fontsize=14, pad=20)
plt.legend(loc="lower right", fontsize=11)
plt.grid(True, alpha=0.3)
plt.show()Матрицы ошибок
Анализ структуры ошибок для каждой модели.
Logistic Regression
plt.figure(figsize=(6, 5))
cm_lr = confusion_matrix(y_test, lr_pred)
sns.heatmap(cm_lr, annot=True, fmt='d', cmap='Blues',
xticklabels=['Нет заболевания', 'Есть заболевание'],
yticklabels=['Нет заболевания', 'Есть заболевание'])
plt.title('Logistic Regression: Матрица ошибок', fontsize=12)
plt.xlabel('Предсказано')
plt.ylabel('Фактически')
plt.tight_layout()
plt.show()Random Forest
plt.figure(figsize=(6, 5))
cm_rf = confusion_matrix(y_test, rf_pred)
sns.heatmap(cm_rf, annot=True, fmt='d', cmap='Blues',
xticklabels=['Нет заболевания', 'Есть заболевание'],
yticklabels=['Нет заболевания', 'Есть заболевание'])
plt.title('Random Forest: Матрица ошибок', fontsize=12)
plt.xlabel('Предсказано')
plt.ylabel('Фактически')
plt.tight_layout()
plt.show()Важность признаков
Анализ того, какие признаки оказали наибольшее влияние на предсказания модели Random Forest.
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)
plt.figure(figsize=(10, 8))
sns.barplot(data=feature_importance, x='importance', y='feature',
palette=['#FF6B6B' if x > 0.1 else '#808080' for x in feature_importance['importance']])
plt.title('Важность признаков (Random Forest)', fontsize=14, pad=20)
plt.xlabel('Важность', fontsize=12)
plt.ylabel('Признак', fontsize=12)
plt.tight_layout()
plt.show()Топ-10 наиболее важных признаков для Random Forest:
GT(feature_importance.head(10))| feature | importance |
|---|---|
| ap_hi | 0.4058238277023232 |
| ap_lo | 0.2129396221097338 |
| age_years | 0.13441021545516213 |
| cholesterol | 0.08758118538511586 |
| bmi | 0.06073565303153275 |
| weight | 0.041346187892077106 |
| height | 0.02503652864542541 |
| gluc | 0.012270997560449512 |
| active | 0.007779912016059097 |
| smoke | 0.004542535941688497 |
Коэффициенты логистической регрессии для интерпретации влияния признаков.
lr_coefficients = pd.DataFrame({
'feature': X.columns,
'coefficient': lr_model.coef_[0],
'abs_coefficient': np.abs(lr_model.coef_[0])
}).sort_values('abs_coefficient', ascending=False)
GT(lr_coefficients.head(10)[['feature', 'coefficient']])| feature | coefficient |
|---|---|
| ap_hi | 0.9364202252354167 |
| cholesterol | 0.4970137211014722 |
| age_years | 0.33885377520495996 |
| active | -0.2280743685924696 |
| alco | -0.21977945603609264 |
| smoke | -0.16551620438076034 |
| weight | 0.13193924230424112 |
| gluc | -0.1315486347478378 |
| ap_lo | 0.10348648737514085 |
| bmi | 0.024231514028308854 |
Детальное сравнение метрик
Построим отдельные графики для каждой метрики.
metrics_list = ['Accuracy', 'Precision', 'Recall', 'F1-Score', 'ROC-AUC', 'Specificity']
models = ['Logistic Regression', 'Random Forest']
colors = ['#808080', '#FF6B6B']Accuracy
val_acc = [lr_metrics['Accuracy'], rf_metrics['Accuracy']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_acc, color=colors)
plt.title('Accuracy')
plt.ylim(0, 1)
for bar, value in zip(bars, val_acc):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Precision
val_prec = [lr_metrics['Precision'], rf_metrics['Precision']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_prec, color=colors)
plt.title('Precision')
plt.ylim(0, 1)
for bar, value in zip(bars, val_prec):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Recall
val_rec = [lr_metrics['Recall'], rf_metrics['Recall']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_rec, color=colors)
plt.title('Recall')
plt.ylim(0, 1)
for bar, value in zip(bars, val_rec):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()F1-Score
val_f1 = [lr_metrics['F1-Score'], rf_metrics['F1-Score']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_f1, color=colors)
plt.title('F1-Score')
plt.ylim(0, 1)
for bar, value in zip(bars, val_f1):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()ROC-AUC
val_auc = [lr_metrics['ROC-AUC'], rf_metrics['ROC-AUC']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_auc, color=colors)
plt.title('ROC-AUC')
plt.ylim(0, 1)
for bar, value in zip(bars, val_auc):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Specificity
val_spec = [lr_metrics['Specificity'], rf_metrics['Specificity']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_spec, color=colors)
plt.title('Specificity')
plt.ylim(0, 1)
for bar, value in zip(bars, val_spec):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Анализ пороговых значений
Исследуем, как меняются метрики при изменении порога классификации.
thresholds = np.arange(0.3, 0.8, 0.05)
def calculate_metrics_at_threshold(y_true, y_proba, threshold):
y_pred = (y_proba >= threshold).astype(int)
return {
'threshold': threshold,
'accuracy': accuracy_score(y_true, y_pred),
'precision': precision_score(y_true, y_pred),
'recall': recall_score(y_true, y_pred),
'f1': f1_score(y_true, y_pred)
}
threshold_metrics_lr = []
threshold_metrics_rf = []
for threshold in thresholds:
threshold_metrics_lr.append(calculate_metrics_at_threshold(y_test, lr_pred_proba, threshold))
threshold_metrics_rf.append(calculate_metrics_at_threshold(y_test, rf_pred_proba, threshold))
df_thresholds_lr = pd.DataFrame(threshold_metrics_lr)
df_thresholds_rf = pd.DataFrame(threshold_metrics_rf)Зависимость метрик от порога: Logistic Regression
plt.figure(figsize=(10, 6))
for metric in ['accuracy', 'precision', 'recall', 'f1']:
plt.plot(df_thresholds_lr['threshold'], df_thresholds_lr[metric],
marker='o', label=metric.capitalize())
plt.axvline(x=0.5, color='red', linestyle='--', alpha=0.7, label='Порог 0.5')
plt.xlabel('Порог классификации')
plt.ylabel('Значение метрики')
plt.title('Logistic Regression: Зависимость метрик от порога')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()Зависимость метрик от порога: Random Forest
plt.figure(figsize=(10, 6))
for metric in ['accuracy', 'precision', 'recall', 'f1']:
plt.plot(df_thresholds_rf['threshold'], df_thresholds_rf[metric],
marker='o', label=metric.capitalize())
plt.axvline(x=0.5, color='red', linestyle='--', alpha=0.7, label='Порог 0.5')
plt.xlabel('Порог классификации')
plt.ylabel('Значение метрики')
plt.title('Random Forest: Зависимость метрик от порога')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()Оптимальные пороги по F1-score:
optimal_threshold_lr = df_thresholds_lr.loc[df_thresholds_lr['f1'].idxmax(), 'threshold']
optimal_threshold_rf = df_thresholds_rf.loc[df_thresholds_rf['f1'].idxmax(), 'threshold']
print(f"Logistic Regression: {optimal_threshold_lr:.3f}")
print(f"Random Forest: {optimal_threshold_rf:.3f}")Logistic Regression: 0.400
Random Forest: 0.350
Обсуждение
Ключевые findings
На основе проведенного анализа данных о сердечно-сосудистых заболеваниях были получены следующие ключевые результаты:
Демографические характеристики
- Сбалансированная выборка: распределение наличия/отсутствия заболевания практически сбалансировано (50.5% пациентов с заболеваниями против 49.5% без)
- Преобладание женщин: в выборке представлено больше женщин, чем мужчин (примерно 65% против 35%)
- Возрастной диапазон: пациенты в возрасте от 40 до 65 лет, что соответствует группе повышенного риска ССЗ
Факторы риска
Наиболее значимыми факторами риска, выявленными в ходе анализа, являются:
- Артериальное давление (систолическое и диастолическое) - самый сильный предиктор
- Возраст - прямо коррелирует с вероятностью заболевания
- Уровень холестерина - второй по важности фактор
- Индекс массы тела (BMI) - избыточный вес и ожирение значимо повышают риск
Качество моделей
Обе модели продемонстрировали качество выше требуемых порогов:
- Random Forest: AUC-ROC = 0.78 (превышает требование > 0.75)
- Logistic Regression: AUC-ROC = 0.76 (соответствует требованию)
Random Forest показывает незначительное преимущество по всем метрикам, однако Logistic Regression обладает лучшей интерпретируемостью.
Практические рекомендации
Для медицинской лаборатории
- Приоритетные показатели: при скрининге следует уделять особое внимание артериальному давлению и уровню холестерина
- Возрастные группы: пациенты старше 50 лет должны находиться в группе повышенного внимания
- BMI мониторинг: регулярный контроль индекса массы тела для своевременного выявления рисков
Критерии выбора модели
- Random Forest рекомендуется для автоматизированного скрининга (более высокая точность)
- Logistic Regression - для клинической практики (интерпретируемость коэффициентов)
Ограничения исследования
- Отсутствие дополнительных факторов: в данных нет информации о наследственности, питании, стрессовых факторах
- Популяционные особенности: датасет может не полностью представлять все демографические группы
- Временные ограничения: данные представляют срез во времени без анализа динамики
Направления для будущих исследований
- Включение генетических маркеров для более точной оценки риска
- Долгосрочное наблюдение за пациентами для оценки прогрессии заболевания
- Интеграция с лабораторными анализами (биохимические показатели крови)
- Разработка интерактивного калькулятора риска для использования клиницистами
Заключение
В ходе данного исследования был проведен комплексный анализ данных сердечно-сосудистых заболеваний с целью выявления ключевых факторов риска и разработки предиктивных моделей.
Основные результаты
Выявлены ключевые факторы риска: артериальное давление, возраст, уровень холестерина и BMI являются наиболее значимыми предикторами наличия ССЗ
Разработаны предиктивные модели: обе модели (Logistic Regression и Random Forest) превышают требуемые пороги качества (AUC-ROC > 0.75)
Обеспечена воспроизводимость: полный анализ документирован с использованием Quarto, что гарантирует воспроизводимость результатов
Созданы практические рекомендации: разработаны конкретные рекомендации для медицинской лаборатории по использованию результатов анализа
Вклад в практику
Результаты исследования могут быть использованы для:
- Оптимизации скрининговых программ - фокус на наиболее информативных показателях
- Персонализации подхода - учет индивидуальных факторов риска пациента
- Повышения эффективности профилактики - своевременное выявление групп риска
- Автоматизации предварительной диагностики - использование ML моделей для поддержки принятия решений
Техническое достижение
Успешно реализован полный цикл анализа данных: от загрузки и очистки до построения и оценки моделей, с созданием полностью воспроизводимого исследования в формате Quarto документа.
Исследование подтверждает эффективность машинного обучения в медицинской диагностике и предоставляет практический инструмент для использования в реальной клинической практике.